﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Threading;
using System.IO;

using Debug = UnityEngine.Debug;

public delegate void Message();

public class VoxelTerrain : MonoBehaviour
{
    private const int DIST_TO_LOAD_NEAR = 80;
    private const int DIST_TO_LOAD = 200;

    public const byte CHUNK_SIZE = 1 << 4;      /// 16
    public const short CHUNK_VOLUMNE = 1 << 12; /// 4096
    public const short REGION_SIZE = 1 << 9;    /// 512
    public const byte WORLD_HEIGHT = 1 << 7;    /// 128
    public const byte CHUNKS_PER_SIDE = 1 * (REGION_SIZE / CHUNK_SIZE); /// 32
    public const byte LAYER_PER_CHUNK = 1 << 3; /// 8
    public static GameObject ChunkPrefab_Static;
    public static Rect[] UV_Coordinates;

    public static VoxelTerrain Instance { get; private set; }

    public static volatile int UpdateMeshCounter = 0;
    public static volatile int GenerateMeshCounter = 0;
    public static volatile int GenerateDataCounter = 0;

    public static volatile float UpdateMeshPerSec = 0;
    public static volatile float GenerateMeshPerSec = 0;

    public static Message OnTerrainChangeDelegate;

    private volatile bool LoadingFlag = false;

    [SerializeField]
    private GameObject ChunkPrefab;

    public Region currentRegion { get; private set; }

    private volatile HashSet<Chunk> InnerRenderList;
    private volatile HashSet<Chunk> OuterRenderList;

    private volatile HashSet<Chunk> OuterRenderList2;
    private volatile HashSet<Chunk> InnerRenderList2;

    private volatile HashSet<Region> RegionList;

    /// <summary>
    /// Contains chunks that are changed during the last frame. They are already rendered.
    /// </summary>
    public HashSet<Chunk> RepaintList;

    /// <summary>
    /// The height of the focused layer inside a chunk
    /// </summary>
    public static int RenderHeight = 5;

    /// <summary>
    /// The amount of layers being rendered above and beneath the RenderHeight
    /// </summary>
    public const int LayerRenderAmount = 2;

    private float timer = 0;

    private void OnDestroy()
    {
        Debug.Log("Programm Canceled!");
        Thread.Sleep(10);
        Application.Quit();
    }

    void Awake()
    {
        Instance = this;
    }

    void Start()
    {
        InnerRenderList = new HashSet<Chunk>();
        OuterRenderList = new HashSet<Chunk>();

        InnerRenderList2 = new HashSet<Chunk>();
        OuterRenderList2 = new HashSet<Chunk>();

        RegionList = new HashSet<Region>();

        RepaintList = new HashSet<Chunk>();

        ChunkPrefab_Static = ChunkPrefab;

        Point3 playerPos = Point3.Round(Camera.main.transform.position);
        LoadChunks(playerPos);
    }

    //private void GenerateChunkData_Worker2(int number)
    //{
    //    Debug.Log(number);
    //    for (int i = 0; i < chunkArray.Length / System.Environment.ProcessorCount; i++)
    //    {
    //        chunkArray[i + (number * (chunkArray.Length / System.Environment.ProcessorCount))].GenerateData();
    //    }
    //}

    void Update()
    {
        if (RepaintList.Count > 0 && OnTerrainChangeDelegate != null)
            OnTerrainChangeDelegate();

        foreach (Chunk chunk in RepaintList)
        {
            chunk.GenerateMesh();
            chunk.UpdateMesh();
        }
        RepaintList.Clear();

        if (timer >= 1f && Time.deltaTime < 1 / 30f)
        {
            UpdateMeshPerSec = UpdateMeshCounter;
            UpdateMeshCounter = 0;

            GenerateMeshPerSec = GenerateMeshCounter;
            GenerateMeshCounter = 0;

            timer -= 1f;
            Stopwatch stopwatch = new Stopwatch();

            stopwatch.Start();
            LoadChunks(Point3.Round(Camera.main.transform.position));
            stopwatch.Stop();
            Debug.Log(stopwatch.ElapsedMilliseconds);

            //Point3 playerPos = Point3.Round(Camera.main.transform.position);
            //new Thread(() => LoadChunks(playerPos)).Start();
        }
        timer += Time.deltaTime;

        Thread workerThread = new Thread(ChunkRepainting_WorkThread);
        workerThread.Start();
        ChunkRepainting();
        workerThread.Join();


        if (InnerRenderList.Count > 0)
        {
            Chunk chunk = InnerRenderList.First();
            chunk.UpdateMesh();
            InnerRenderList.Remove(chunk);
        }
        else if (OuterRenderList.Count > 0)
        {
            Chunk chunk = OuterRenderList.First();
            chunk.UpdateMesh();
            OuterRenderList.Remove(chunk);
        }

        if (InnerRenderList2.Count > 0)
        {
            Chunk chunk = InnerRenderList2.First();
            chunk.UpdateMesh();
            InnerRenderList2.Remove(chunk);
        }
        else if (OuterRenderList2.Count > 0)
        {
            Chunk chunk = OuterRenderList2.First();
            chunk.UpdateMesh();
            OuterRenderList2.Remove(chunk);
        }
    }

    private void ChunkRepainting_WorkThread()
    {
        if (InnerRenderList2.Count > 0)
        {
            InnerRenderList2.First().GenerateMesh();
        }
        else if (OuterRenderList2.Count > 0)
        {
            OuterRenderList2.First().GenerateMesh();
        }
    }
    private void ChunkRepainting()
    {
        if (InnerRenderList.Count > 0)
        {
            InnerRenderList.First().GenerateMesh();
        }
        else if (OuterRenderList.Count > 0)
        {
            OuterRenderList.First().GenerateMesh();
        }
    }

    public Region GetRegionAt(Point3 point)
    {
        return GetRegionAt(point.x, point.z);
    }
    public Region GetRegionAt(int x, int z)
    {
        foreach (Region region in RegionList)
        {
            if (region.IsInside(x, z))
                return region;
        }

        Region result = Region.FindOrCreate(x, z);
        RegionList.Add(result);

        return result;
    }

    private void LoadChunks(Point3 playerPos)
    {
        currentRegion = GetRegionAt(playerPos);
        //new Thread(currentRegion.WriteRegionDataToFile).Start();

        //RegionList.Clear();
        RegionList.Add(currentRegion);
        Region tmpRegion = null;

        // right
        if (!currentRegion.IsInside(playerPos.x + DIST_TO_LOAD, playerPos.z))
            RegionList.Add(GetRegionAt(playerPos.x + DIST_TO_LOAD, playerPos.z));

        // left
        if (!currentRegion.IsInside(playerPos.x - DIST_TO_LOAD, playerPos.z))
            RegionList.Add(GetRegionAt(playerPos.x - DIST_TO_LOAD, playerPos.z));

        // top
        if (!currentRegion.IsInside(playerPos.x, playerPos.z + DIST_TO_LOAD))
            RegionList.Add(GetRegionAt(playerPos.x, playerPos.z + DIST_TO_LOAD));

        // bottom
        if (!currentRegion.IsInside(playerPos.x, playerPos.z - DIST_TO_LOAD))
            RegionList.Add(GetRegionAt(playerPos.x, playerPos.z - DIST_TO_LOAD));

        // top right
        if (!currentRegion.IsInside(playerPos.x + DIST_TO_LOAD, playerPos.z + DIST_TO_LOAD))
            RegionList.Add(GetRegionAt(playerPos.x + DIST_TO_LOAD, playerPos.z + DIST_TO_LOAD));

        // top left
        if (!currentRegion.IsInside(playerPos.x - DIST_TO_LOAD, playerPos.z + DIST_TO_LOAD))
            RegionList.Add(GetRegionAt(playerPos.x - DIST_TO_LOAD, playerPos.z + DIST_TO_LOAD));

        // bottom right
        if (!currentRegion.IsInside(playerPos.x + DIST_TO_LOAD, playerPos.z - DIST_TO_LOAD))
            RegionList.Add(GetRegionAt(playerPos.x + DIST_TO_LOAD, playerPos.z - DIST_TO_LOAD));

        // bottom left
        if (!currentRegion.IsInside(playerPos.x - DIST_TO_LOAD, playerPos.z - DIST_TO_LOAD))
            RegionList.Add(GetRegionAt(playerPos.x - DIST_TO_LOAD, playerPos.z - DIST_TO_LOAD));

        //Debug.Log(RegionList.Count);

        Chunk tmpChunk = null;

        for (int x = -(DIST_TO_LOAD / CHUNK_SIZE); x < (DIST_TO_LOAD / CHUNK_SIZE); x++)
            for (int z = -(DIST_TO_LOAD / CHUNK_SIZE); z < (DIST_TO_LOAD / CHUNK_SIZE); z++)
                for (int y = 0; y < LAYER_PER_CHUNK; y++)
                {
                    Point3 tmpChunkPos = new Point3(playerPos.x + x * CHUNK_SIZE, y * CHUNK_SIZE, playerPos.z + z * CHUNK_SIZE);

                    tmpRegion = null;
                    foreach (Region region in RegionList)
                    {
                        if (region.IsInside(tmpChunkPos.x, tmpChunkPos.z))
                        {
                            tmpRegion = region;
                            break;
                        }
                    }
                    if (tmpRegion == null)
                    {
                        tmpRegion = GetRegionAt(x * CHUNK_SIZE, z * CHUNK_SIZE);
                        RegionList.Add(tmpRegion);
                    }

                    tmpChunk = tmpRegion.GetChunkAt(tmpChunkPos);

                    HashSet<Chunk> innerlist = GetInnerRenderList();
                    HashSet<Chunk> outerList = GetOuterRenderList();

                    if (tmpChunk == null || tmpChunk.IsClean && (!innerlist.Contains(tmpChunk) || !outerList.Contains(tmpChunk)))
                    {

                        if (Point2.SqrDistance(new Point2(playerPos.x, playerPos.z), new Point2(tmpChunkPos.x, tmpChunkPos.z)) < DIST_TO_LOAD_NEAR * DIST_TO_LOAD_NEAR)
                        {
                            GenerateChunkColumnInInnerList(tmpChunkPos.x, tmpChunkPos.z, tmpRegion);
                        }
                        else
                        {
                            GenerateChunkColumnInOuterList(tmpChunkPos.x, tmpChunkPos.z, tmpRegion);
                        }
                    }
                }
    }
    private void GenerateChunkColumnInInnerList(int x, int z, Region region)
    {
        Chunk tmpChunk = null;
        for (int y = 0; y < LAYER_PER_CHUNK; y++)
        {
            tmpChunk = region.GetChunkAt(x, y * CHUNK_SIZE, z);

            HashSet<Chunk> innerList = GetInnerRenderList();

            if (!innerList.Contains(tmpChunk))
            {
                innerList.Add(tmpChunk);
            }
            GetOuterRenderList().Remove(tmpChunk);
        }
    }
    private void GenerateChunkColumnInOuterList(int x, int z, Region region)
    {
        Chunk tmpChunk = null;
        for (int y = 0; y < LAYER_PER_CHUNK; y++)
        {
            tmpChunk = region.GetChunkAt(x, y * CHUNK_SIZE, z);

            HashSet<Chunk> outerList = GetOuterRenderList();

            if (!outerList.Contains(tmpChunk))
            {
                outerList.Add(tmpChunk);
            }
            GetInnerRenderList().Remove(tmpChunk);
        }
    }

    private void UnloadChunkColumnAt(int x, int z, Region region)
    {
        Chunk tmpChunk = null;
        for (int y = 0; y < LAYER_PER_CHUNK; y++)
        {
            tmpChunk = region.GetChunkAt(x, y * CHUNK_SIZE, z);

            InnerRenderList.Remove(tmpChunk);
            InnerRenderList2.Remove(tmpChunk);
            OuterRenderList.Remove(tmpChunk);
            OuterRenderList2.Remove(tmpChunk);

            if (tmpChunk != null && !tmpChunk.IsClean)
            {
                tmpChunk.CleanUp();
            }
        }
    }

    private HashSet<Chunk> GetInnerRenderList()
    {
        return InnerRenderList.Count <= InnerRenderList2.Count ? InnerRenderList : InnerRenderList2;
        //return InnerDataCalcList;
        //return InnerDataCalcList.Count <= InnerRenderList2.Count ? InnerDataCalcList : InnerRenderList2;
    }
    private HashSet<Chunk> GetOuterRenderList()
    {
        return OuterRenderList.Count <= OuterRenderList2.Count ? OuterRenderList : OuterRenderList2;
        //return OuterDataCalcList;
        //return OuterDataCalcList.Count <= OuterRenderList2.Count ? OuterDataCalcList : OuterRenderList2;
    }


    /// <summary>
    /// Returns the TextureID for a block in world space
    /// </summary>
    /// <param name="x">The x-coordinate in world space</param>
    /// <param name="y">The y-coordinate in world space</param>
    /// <param name="z">The z-coordinate in world space</param>
    public byte GetBlockAt(int x, int y, int z)
    {
        if (y >= WORLD_HEIGHT || y < 0)
        {
            return Block.Stone;
        }

        return GetRegionAt(x, z).GetChunkAt(x, y, z).GetBlockAt(x % CHUNK_SIZE, y % CHUNK_SIZE, z % CHUNK_SIZE);
    }
    /// <summary>
    /// Returns the TextureID for a block in world space
    /// </summary>
    /// <param name="pos">The position in world space</param>
    public byte GetBlockAt(Point3 pos)
    {
        return GetBlockAt(pos.x, pos.y, pos.z);
    }
    public byte GetBlockAt(int x, int y, int z, Region region)
    {
        return region.GetChunkAt(x, y, z).GetBlockAt(x % CHUNK_SIZE, y % CHUNK_SIZE, z % CHUNK_SIZE);
    }

    public void SetBlockAt(int x, int y, int z, byte block)
    {
        if (y >= WORLD_HEIGHT || y < 0)
            throw new System.ArgumentException(string.Format("Bad coordinates trying to set a block at {0}", new Point3(x, y, z)));

        GetChunkAt(x, y, z).SetBlockAt(x % CHUNK_SIZE, y % CHUNK_SIZE, z % CHUNK_SIZE, block);
    }
    public void SetBlockAt(Point3 pos, byte block)
    {
        SetBlockAt(pos.x, pos.y, pos.z, block);
    }

    /// <summary>
    /// Try to destroy a block and return success of it
    /// </summary>
    /// <param name="x">The x-coordinate in world space</param>
    /// <param name="y">The y-coordinate in world space</param>
    /// <param name="z">The z-coordinate in world space</param>
    public bool TryDestroyBlockAt(int x, int y, int z)
    {
        // TODO Do magic gameplay stuff here
        if (y >= WORLD_HEIGHT || y <= 0)
            return false;

        Chunk chunk = GetChunkAt(x, y, z);

        chunk.SetBlockAt(x % CHUNK_SIZE, y % CHUNK_SIZE, z % CHUNK_SIZE, Block.Air);

        if (chunk.IsClean == false)
        {
            chunk.Repaint();

            #region Adjacent chunk repainting
            if (x % VoxelTerrain.CHUNK_SIZE == 0)
                chunk.GetLeftChunk().Repaint();
            if (x % VoxelTerrain.CHUNK_SIZE == VoxelTerrain.CHUNK_SIZE - 1)
                chunk.GetRightChunk().Repaint();

            if (y >= VoxelTerrain.CHUNK_SIZE && y % VoxelTerrain.CHUNK_SIZE == 0)
                chunk.GetBottomChunk().Repaint();
            if (y <= VoxelTerrain.WORLD_HEIGHT - VoxelTerrain.CHUNK_SIZE && y % VoxelTerrain.CHUNK_SIZE == VoxelTerrain.CHUNK_SIZE - 1)
                chunk.GetTopChunk().Repaint();

            if (z % VoxelTerrain.CHUNK_SIZE == 0)
                chunk.GetBackChunk().Repaint();
            if (z % VoxelTerrain.CHUNK_SIZE == VoxelTerrain.CHUNK_SIZE - 1)
                chunk.GetFrontChunk().Repaint();
            #endregion
        }


        return true;
    }
    /// <summary>
    /// Destroys a block
    /// </summary>
    /// <param name="pos">The position in world space</param>
    /// <returns>True if destruction was successfull</returns>
    public bool TryDestroyBlockAt(Point3 pos)
    {
        return TryDestroyBlockAt(pos.x, pos.y, pos.z);
    }

    /// <summary>
    /// Returns a chunk at given point in world space
    /// </summary>
    /// <param name="x">The x-coordinate of the chunk in world space</param>
    /// <param name="y">The y-coordinate of the chunk in world space</param>
    /// <param name="z">The z-coordinate of the chunk in world space</param>
    /// <returns></returns>
    public Chunk GetChunkAt(int x, int y, int z)
    {
        return GetRegionAt(x, z).GetChunkAt(x, y, z);
    }
    /// <summary>
    /// Returns a chunk at given point in world space
    /// </summary>
    /// <param name="pos">The given point in world space</param>
    public Chunk GetChunkAt(Point3 pos)
    {
        return GetChunkAt(pos.x, pos.y, pos.z);
    }

    //public void CleanUp()
    //{
    //    // Clean up Chunks
    //    if (chunkArray != null)
    //        foreach (Chunk chunk in chunkArray)
    //        {
    //            if (chunk != null)
    //                chunk.CleanUp();
    //        }
    //    Chunk.ID = 0;

    //    List<GameObject> tmp = new List<GameObject>();
    //    foreach (Transform child in transform) tmp.Add(child.gameObject);
    //    tmp.ForEach(child => DestroyImmediate(child));
    //}

    //public void WriteToFile()
    //{
    //    using (BinaryWriter writer = new BinaryWriter(File.Open(string.Format("r.{0}.{1}.rgn", 0, 0), FileMode.Create, FileAccess.Write)))
    //    {
    //        for (int i = 0; i < chunkArray.Length; i++)
    //        {
    //            foreach (Block blockID in chunkArray[i].chunkData)
    //            {
    //                writer.Write(blockID.GetValue());
    //            }
    //        }
    //    }
    //}

    private byte ReadBlockFromFile(int posX, int posY, int posZ)
    {
        int chunkPosX = posX / CHUNK_SIZE;
        int chunkPosY = posY / CHUNK_SIZE;
        int chunkPosZ = posZ / CHUNK_SIZE;

        int offsetX = posX & (CHUNK_SIZE - 1); // posX % CHUNK_SIZE (16)
        int offsetY = posY & (CHUNK_SIZE - 1); // posY % CHUNK_SIZE (16)
        int offsetZ = posZ & (CHUNK_SIZE - 1); // posZ % CHUNK_SIZE (16)

        using (BinaryReader reader = new BinaryReader(File.OpenRead(string.Format("r.{0}.{1}.rgn", posX / REGION_SIZE, posY / REGION_SIZE))))
        {
            int pos = (chunkPosX + (chunkPosY * (REGION_SIZE / CHUNK_SIZE)) + (chunkPosZ * CHUNK_SIZE * CHUNK_SIZE)) * 4096; // Chunk Start Point
            pos += offsetX + (offsetY * CHUNK_SIZE) + (offsetZ * CHUNK_SIZE * CHUNK_SIZE); // Offset inside the chunk
            reader.BaseStream.Position = pos;

            return reader.ReadByte();
        }
    }

    /// <summary>
    /// Returns the height of the first block not equal to Air
    /// </summary>
    /// <param name="voxelTerrain"></param>
    /// <param name="x">The x-coordinate in world space</param>
    /// <param name="z">The z-coordinate in world space</param>
    /// <returns></returns>
    public byte HeightmapLayer(int x, int z)
    {
        for (byte y = WORLD_HEIGHT - 1; y >= 0; y--)
        {
            if (GetBlockAt(x, y, z) != Block.Air)
                return y;
        }

        return 0;
    }

    private void OnGUI()
    {
        GUILayout.Label(GenerateMeshCounter.ToString());
        GUILayout.Label(UpdateMeshCounter.ToString());
        GUILayout.Label(GenerateMeshPerSec.ToString());
        GUILayout.Label(UpdateMeshPerSec.ToString());
    }
}
